// Copyright (C) 2005-2008 Jae Stutzman (jaebird@gmail.com)
//
// This file is part of Podducer.
//
// Podducer is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Podducer is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Podducer.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections;
using System.IO;
using System.Data;
using System.Diagnostics;
using System.Threading;

namespace Podducer
{
	public delegate void ControllerHandler();
	public delegate void VuMeasureHandler(int val);

	public enum RecordMode
	{
		Stopped,
		Paused,
		Recording,
		Stopping
	}

	public class RecordController : IDisposable
	{
		public event ControllerHandler RecordOccured;
		public event ControllerHandler PauseOccured;
		public event ControllerHandler StopOccured;
		public event VuMeasureHandler VuMeasureOccured;

		private DateTime recStartTime;
		private DateTime pauseStartTime;
		private TimeSpan pauseTimeSpan;

		private const string sndFilename = "sndconsole.exe";

		private readonly string stopFile = Environment.CurrentDirectory + @"\sndconsole.stop.flag";
		private readonly string pauseFile = Environment.CurrentDirectory + @"\sndconsole.pause.flag";

		private Process sndProcess;
		private Process encProcess;

		private string filenameMask = string.Empty;
		private string destinationDirectory = string.Empty;
		private int numberOfChannels = 1;
		private int bitsPerSample = 16;
		private int sampleRate = 44100;
		private TimeSpan maxRecordingTime = new TimeSpan(1, 0 , 0);
		private string recFilename = string.Empty;
		private string selectedEncoder = "lame";
		private string encoderFilename = "lame.exe";
		private string encoderArguments;


		private string qualitySetting = "16";
		private string customEncoder = string.Empty;
		private string fileExtension = ".mp3";

		public volatile int meter;

		public string Filename
		{
			get
			{
				return recFilename;
			}
		}

		public string DestinationDirectory
		{
			get
			{
				return destinationDirectory;
			}
		}

		private RecordMode recordMode;

		public RecordMode RecordMode
		{
			get
			{
				return recordMode;
			}
		}

		public RecordController()
		{
			if (File.Exists(stopFile))
			{
				File.Delete(stopFile);
			}

			if (File.Exists(pauseFile))
			{
				File.Delete(pauseFile);
			}

			RefreshConfiguration();
		}

		public bool StartRecord()
		{
			if (!File.Exists(sndFilename) || !File.Exists(encoderFilename))
			{
				return false;
			}

			string filename = CreateFilename();
			int index = 0;
			recFilename = Path.Combine(destinationDirectory, filename + fileExtension);
			while (File.Exists(recFilename))
			{
				index++;
				recFilename = Path.Combine(destinationDirectory, filename + "(" + index + ")" + fileExtension);
			}

			string sndArguments = "-B " + bitsPerSample.ToString() + " -C " + numberOfChannels.ToString() + " -R " + sampleRate.ToString();
			
			string encoderCompleteArguments = encoderArguments + "\"" + recFilename + "\"";

			sndProcess = new Process();
			sndProcess.StartInfo.UseShellExecute = false;
			sndProcess.StartInfo.RedirectStandardInput = true;
			sndProcess.StartInfo.RedirectStandardOutput = true;
			sndProcess.StartInfo.CreateNoWindow = true;
			sndProcess.StartInfo.FileName = sndFilename;
			sndProcess.StartInfo.Arguments = sndArguments;
			//sndProcess.StartInfo.WorkingDirectory = WorkingDirectory;

			encProcess = new Process();
			encProcess.StartInfo.UseShellExecute = false;
			encProcess.StartInfo.RedirectStandardInput = true;
			encProcess.StartInfo.CreateNoWindow = true;
			encProcess.StartInfo.FileName = encoderFilename;
			encProcess.StartInfo.Arguments = encoderCompleteArguments;
			//encProcess.StartInfo.WorkingDirectory = WorkingDirectory;

			try
			{
				sndProcess.Start();
				encProcess.Start();

				encProcess.StandardInput.AutoFlush = true;

				Thread t = new Thread(new ThreadStart(Process));
				t.Start();

				recordMode = RecordMode.Recording;

				pauseTimeSpan = new TimeSpan();
				recStartTime = DateTime.Now;

				if (RecordOccured != null)
				{
					RecordOccured();
				}
			}
			catch (Exception exception)
			{
				try
				{
					sndProcess.Kill();
				}
				catch{}

				try
				{
					encProcess.Kill();
				}
				catch{}

				return false;
			}
			return true;
		}

		private void Process()
		{
			byte[] buff = new byte [24000];
			int numberOfBytesRead = 0;
			do
			{
				// calculate the recording timespan
				TimeSpan recDurationTimeSpan = DateTime.Now - recStartTime - pauseTimeSpan;
				// if we have reached the limit, stop the recording
				if (recDurationTimeSpan > maxRecordingTime)
				{
					StopRecording();
				}

				// read the bytes from the sndconsole...
				numberOfBytesRead = sndProcess.StandardOutput.BaseStream.Read(buff, 0, 24000);

				// if the event has an invocation list...
				if (VuMeasureOccured != null)
				{
					// fire the VU event (this is for the VU meter)
					VuMeasureOccured(SetVU(buff, numberOfBytesRead));
				}

				// write the bytes to the encoder stdin
				encProcess.StandardInput.BaseStream.Write(buff, 0, numberOfBytesRead);

			} while (numberOfBytesRead > 0); // as long as there are bytes that we've read
			
			// close everything down
			encProcess.StandardInput.BaseStream.Close();

			sndProcess.WaitForExit();

			if (File.Exists(stopFile))
			{
				File.Delete(stopFile);
			}

			if (File.Exists(pauseFile))
			{
				File.Delete(pauseFile);
			}

			recordMode = RecordMode.Stopped;

			if (StopOccured != null)
			{
				StopOccured();
			}

		}

		private int SetVU(byte[] buff, int length)
		{
			int left = 0, right = 0;

			if (bitsPerSample == 16)
			{
				for(int n = 0; n < length; n += bitsPerSample / 8 * numberOfChannels)
				{
					int blipValue = (Int16)(buff[n] | (buff[n+1] << 8));
					if(left < Math.Abs(blipValue))
						left = Math.Abs(blipValue);
		
					//if(right < Math.Abs(buff[n+1]))
					//	right = Math.Abs(buff[n+1]);
				}
				//right = (int)(Math.Sqrt((float)right/System.Int16.MaxValue) * 50.0);
				left = (int)(Math.Sqrt((float)left/System.Int16.MaxValue) * 50.0);
			}
			else if (bitsPerSample == 8)
			{
				for(int n = 0; n < length; n += bitsPerSample / 8 * numberOfChannels)
				{
					int val = buff[n] & 0x7F;//nBitTwosComplement(buff[n], 8);

					//Console.WriteLine(buff[n]);
					if(left < Math.Abs(val))
						left = Math.Abs(val);
		
					//if(right < Math.Abs(buff[n+1]))
					//	right = Math.Abs(buff[n+1]);
				}
				//right = (int)(Math.Sqrt((float)right/System.Int16.MaxValue) * 50.0);
				left = (int)(Math.Sqrt((float)left/0x7F) * 50.0);
			}

			return left;
		}

		public void StopRecording()
		{
			recordMode = RecordMode.Stopping;

			File.Create(stopFile).Close();
		}

		public void PauseOrResume()
		{
			// if it is recording
			if (!File.Exists(pauseFile))
			{
				File.Create(pauseFile).Close();
				recordMode = RecordMode.Paused;

				pauseStartTime = DateTime.Now;
			}
			else
			{
				File.Delete(pauseFile);
				recordMode = RecordMode.Recording;

				pauseTimeSpan += DateTime.Now - pauseStartTime;
			}

			if (PauseOccured != null)
			{
				PauseOccured();
			}
		}

		private string CreateFilename()
		{
			// should use regex here...

			string newMask = filenameMask.Replace("{", "{@");
			string [] maskArray = newMask.Split('{','}');

			string filename = string.Empty;

			foreach (string s in maskArray)
			{
				if (s.StartsWith("@"))
				{
					string temp = s.TrimStart('@');
					filename += DateTime.Now.ToString(temp);
				}
				else
				{
					filename += s;
				}

			}
			
			return filename;
		}

		public void RefreshConfiguration()
		{
			sampleRate = AppConfigure.GetAppSetting("SampleRate", 44100);
			bitsPerSample = AppConfigure.GetAppSetting("BitsPerSample", 16);
			numberOfChannels = AppConfigure.GetAppSetting("NumberOfChannels", 2);

			filenameMask = AppConfigure.GetAppSetting("FilenameMask", string.Empty);
			destinationDirectory = AppConfigure.GetAppSetting("DestinationDirectory", string.Empty);
			
			maxRecordingTime = TimeSpan.Parse(AppConfigure.GetAppSetting("MaxRecordingTime", "00:00"));
			selectedEncoder = AppConfigure.GetAppSetting("SelectedEncoder");
			
			switch (selectedEncoder)
			{
				case EncoderDefs.Lame :
				{
					encoderFilename = "lame.exe";
					qualitySetting = AppConfigure.GetAppSetting("lameQuality");

					// -x = swap bytes
					// --bitwidth = 8 or 16
					// -m = stereo mode j (joint) or m (mono)
					// -r = raw PCM mode
					// -S = silent stdout mode
					// -b = bitrate 8-320
					// -s = sampling frequency in kHz (44.1, 32, 22.05, 11.025)
					// - = use stdin (ie pipe into lame)
					// last param is output filename
					string stereoMode = numberOfChannels == 1 ? "m" : "j";
					encoderArguments = "-r -S -x --bitwidth " + 
						bitsPerSample.ToString() + 
						" -m " + stereoMode + 
						" -s " + (sampleRate / 1000.0).ToString() + 
						" -b " + qualitySetting.ToString() + " - ";

					fileExtension = ".mp3";
					break;
				}
				case EncoderDefs.OggVorbis :
				{
					encoderFilename = "oggenc.exe";
					qualitySetting = AppConfigure.GetAppSetting("oggencQuality");

					encoderArguments = "-r " +
						" -B " + bitsPerSample.ToString() +
						" -C " + numberOfChannels.ToString() +
						" -R " + sampleRate.ToString() +
						" -q " + qualitySetting + 
						" - -o ";

					fileExtension = ".ogg";
					break;
				}
				case EncoderDefs.Speex :
				{
					encoderFilename = "speexenc.exe";
					qualitySetting = AppConfigure.GetAppSetting("speexencQuality");

					encoderArguments = "--quality " + qualitySetting +
						" -u --rate " + sampleRate.ToString() +
						" --" + bitsPerSample.ToString() + "bit" +
						(numberOfChannels > 1 ? "--stereo" : string.Empty) +
						" --vbr - ";


					fileExtension = ".spx";
					break;
				}
				case EncoderDefs.CustomEncoder :
				{
					qualitySetting = string.Empty;
					customEncoder = AppConfigure.GetAppSetting("CustomEncoder");
					break;
				}
			}
		}

		public void Dispose()
		{
			if (recordMode == RecordMode.Recording || recordMode == RecordMode.Paused)
			{
				StopRecording();
			}
		}

		private static int BitTwosComplement(int data, int numBits)
		{
			BitArray b = new BitArray(new int[] { data });

			int result = - (b[numBits - 1] ? 1 : 0) * (1 << numBits - 1);

			for (int i = numBits - 2; i >= 0; i--)
				result += (b[i] ? 1 : 0) * (1 << i);

			return result;
		}

//		private static int BitTwosComplement(int data, int numBits)
//		{
//			int result = - (data & (1 << numBits - 1));
//			for(int i = numBits - 2; i >= 0; i--) 
//				result |= (data & (1 << i)); 
//			return result; 
//		}	
	}
}
